/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.handler.codec;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.internal.ObjectUtil;

import java.nio.ByteOrder;
import java.util.List;

import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;


/**
 * 一个在消息前添加消息长度的编码器. 长度值以二进制形式添加.
 * <p>
 * 例如, <tt>{@link LengthFieldPrepender}(2)</tt> 将会编码以下 12-bytes 字符串:
 * 编码前
 * <pre>
 * +----------------+
 * | "HELLO, WORLD" |
 * +----------------+
 * </pre>
 * 编码后:
 * <pre>
 * +--------+----------------+
 * + 0x000C | "HELLO, WORLD" |
 * +--------+----------------+
 * </pre>
 * 如果你启用构造器中的 {@code lengthIncludesLengthFieldLength} 标记, 编码数据如下所示:
 * (12 (原始数据) + 2 (前置数据) = 14 (0xE)):
 * <pre>
 * +--------+----------------+
 * + 0x000E | "HELLO, WORLD" |
 * +--------+----------------+
 * </pre>
 */
@Sharable
public class LengthFieldPrepender extends MessageToMessageEncoder<ByteBuf> {

    private final ByteOrder byteOrder;
    private final int lengthFieldLength;
    private final boolean lengthIncludesLengthFieldLength;
    private final int lengthAdjustment;

    /**
     * Creates a new instance.
     *
     * @param lengthFieldLength the length of the prepended length field.
     *                          Only 1, 2, 3, 4, and 8 are allowed.
     * @throws IllegalArgumentException if {@code lengthFieldLength} is not 1, 2, 3, 4, or 8
     */
    public LengthFieldPrepender(int lengthFieldLength) {
        this(lengthFieldLength, false);
    }

    /**
     * Creates a new instance.
     *
     * @param lengthFieldLength               the length of the prepended length field.
     *                                        Only 1, 2, 3, 4, and 8 are allowed.
     * @param lengthIncludesLengthFieldLength if {@code true}, the length of the prepended
     *                                        length field is added to the value of the
     *                                        prepended length field.
     * @throws IllegalArgumentException if {@code lengthFieldLength} is not 1, 2, 3, 4, or 8
     */
    public LengthFieldPrepender(int lengthFieldLength, boolean lengthIncludesLengthFieldLength) {
        this(lengthFieldLength, 0, lengthIncludesLengthFieldLength);
    }

    /**
     * Creates a new instance.
     *
     * @param lengthFieldLength the length of the prepended length field.
     *                          Only 1, 2, 3, 4, and 8 are allowed.
     * @param lengthAdjustment  the compensation value to add to the value
     *                          of the length field
     * @throws IllegalArgumentException if {@code lengthFieldLength} is not 1, 2, 3, 4, or 8
     */
    public LengthFieldPrepender(int lengthFieldLength, int lengthAdjustment) {
        this(lengthFieldLength, lengthAdjustment, false);
    }

    /**
     * Creates a new instance.
     *
     * @param lengthFieldLength               the length of the prepended length field.
     *                                        Only 1, 2, 3, 4, and 8 are allowed.
     * @param lengthAdjustment                the compensation value to add to the value
     *                                        of the length field
     * @param lengthIncludesLengthFieldLength if {@code true}, the length of the prepended
     *                                        length field is added to the value of the
     *                                        prepended length field.
     * @throws IllegalArgumentException if {@code lengthFieldLength} is not 1, 2, 3, 4, or 8
     */
    public LengthFieldPrepender(int lengthFieldLength, int lengthAdjustment, boolean lengthIncludesLengthFieldLength) {
        this(ByteOrder.BIG_ENDIAN, lengthFieldLength, lengthAdjustment, lengthIncludesLengthFieldLength);
    }

    /**
     * Creates a new instance.
     *
     * @param byteOrder                       长度字段的  {@link ByteOrder}
     * @param lengthFieldLength               前置长度字段自身的长度. 仅允许设定为 1, 2, 3, 4, and 8
     * @param lengthAdjustment                要添加到长度字段值的补偿值
     * @param lengthIncludesLengthFieldLength {@code true}: 前置长度字段自身的长度将被添加到前置长度的值中.
     *                                        prepended length field.
     * @throws IllegalArgumentException if {@code lengthFieldLength} is not 1, 2, 3, 4, or 8
     */
    public LengthFieldPrepender(ByteOrder byteOrder, int lengthFieldLength, int lengthAdjustment,
                                boolean lengthIncludesLengthFieldLength) {
        if (lengthFieldLength != 1
                && lengthFieldLength != 2
                && lengthFieldLength != 3
                && lengthFieldLength != 4
                && lengthFieldLength != 8) {
            throw new IllegalArgumentException(
                    "lengthFieldLength must be either 1, 2, 3, 4, or 8: " +
                            lengthFieldLength);
        }
        ObjectUtil.checkNotNull(byteOrder, "byteOrder");

        this.byteOrder = byteOrder;
        this.lengthFieldLength = lengthFieldLength;
        this.lengthIncludesLengthFieldLength = lengthIncludesLengthFieldLength;
        this.lengthAdjustment = lengthAdjustment;
    }

    @Override
    protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        int length = msg.readableBytes() + lengthAdjustment;
        if (lengthIncludesLengthFieldLength) {
            length += lengthFieldLength;
        }

        checkPositiveOrZero(length, "length");

        switch (lengthFieldLength) {
            case 1:
                // 校验是否超过可表示范围
                if (length >= 256) {
                    throw new IllegalArgumentException(
                            "length does not fit into a byte: " + length);
                }
                out.add(
                        ctx.alloc()
                                // 分配 buffer
                                .buffer(1)
                                // 设定 order
                                .order(byteOrder)
                                // 写入前置长度字段值
                                .writeByte((byte) length)
                );
                break;
            case 2:
                if (length >= 65536) {
                    throw new IllegalArgumentException(
                            "length does not fit into a short integer: " + length);
                }
                out.add(ctx.alloc().buffer(2).order(byteOrder).writeShort((short) length));
                break;
            case 3:
                if (length >= 16777216) {
                    throw new IllegalArgumentException(
                            "length does not fit into a medium integer: " + length);
                }
                out.add(ctx.alloc().buffer(3).order(byteOrder).writeMedium(length));
                break;
            case 4:
                out.add(ctx.alloc().buffer(4).order(byteOrder).writeInt(length));
                break;
            case 8:
                out.add(ctx.alloc().buffer(8).order(byteOrder).writeLong(length));
                break;
            default:
                throw new Error("should not reach here");
        }
        // 写入 body
        out.add(msg.retain());
    }
}
